package net.beyondts.mplug;

import net.beyondts.mplug.annotation.Plugin;
import net.beyondts.mplug.utils.IOUtils;
import net.beyondts.mplug.utils.JarUtils;
import net.beyondts.mplug.utils.MethodUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;

/**
 * Project: mplug-project<br/>
 * Description: This is the information of a plugin.<br/>
 * Created at: 2019-1-12<br/>
 * Owned by beyondts.
 *
 * @author blazecrystal
 * @version 1.0.0
 */
public class PluginInfo {
    private static Logger LOGGER = LoggerFactory.getLogger(PluginInfo.class);

    // the ArtifactId of the plugin.
    private String artifactId;
    // the version of current plugin.
    private String version;
    // the readable name of current plugin.
    private String name;
    // the description of current plugin.
    private String description;
    // the class loader current plugin will be load in.
    private PluginLoader loader;
    // the absolute path of current plugin's jar file.
    private String mainJar;
    // all plugin methods, key is the extension id.
    private Map<String, List<Method>> pluginMethods;
    // sqls to be executed while plugin installed
    private List<String> installSqls;
    // sqls to be executed while plugin uninstalled
    private List<String> uninstallSqls;

    private PluginInfo() {
        this.pluginMethods = new HashMap<>();
    }

    /**
     * Load plugin's main jar. A plugin should only have one main jar, all @Plugin annotated methods should be contained in this jar.
     * @param mainJar String absolute path of the main jar
     * @return PluginInfo information of current plugin
     * @throws IOException ...
     * @throws ClassNotFoundException ...
     */
    public static PluginInfo load(String mainJar) throws IOException, ClassNotFoundException {
        PluginInfo info = new PluginInfo();
        info.setMainJar(mainJar);
        PluginLoader loader = AccessController.doPrivileged(new PrivilegedAction<PluginLoader>() {
            @Override
            public PluginLoader run() {
                return new PluginLoader(ClassLoader.getSystemClassLoader());
            }
        });
        loader.add(mainJar);
        info.setLoader(loader);
        readPluginInfo(info, loader);
        info.setInstallSqls(readSqls(loader, "install.sql"));
        info.setUninstallSqls(readSqls(loader, "uninstall.sql"));
        info.scanPluginMethods();
        return info;
    }

    private static List<String> readSqls(ClassLoader loader, String file) throws IOException {
        BufferedReader br = null;
        InputStream is = null;
        try {
            is = loader.getResourceAsStream("META-INF/" + file);
            if (is == null) {
                return null;
            }
            br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            List<String> sqls = new ArrayList<>();
            StringBuffer sql = new StringBuffer();
            String line = null;
            while ((line = br.readLine()) != null) {
                line = line.replaceAll("#.*$", "");
                if (StringUtils.isBlank(line)) {
                    continue;
                }
                line = line.trim();
                if (line.endsWith(";")) {
                    line = line.substring(0, line.length() - 1);
                    sql.append(line);
                    sqls.add(new String(sql));
                    sql = new StringBuffer();
                } else {
                    sql.append(line);
                }
            }
            return sqls;
        } finally {
            IOUtils.closeQuietly(br);
            IOUtils.closeQuietly(is);
        }
    }

    private static void readPluginInfo(PluginInfo info, ClassLoader loader) throws IOException {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(loader.getResourceAsStream("META-INF/plugin" +
                    ".info"), "UTF-8"));
            String line = null;
            while ((line = br.readLine()) != null) {
                line = line.replaceAll("#.*$", "");
                if (StringUtils.isBlank(line)) {
                    continue;
                }
                String[] tmp = line.split(":");
                String key = tmp[0].trim();
                String value = tmp[1].trim();
                if ("artifactId".equalsIgnoreCase(key)) {
                    info.setArtifactId(value);
                } else if ("version".equalsIgnoreCase(key)) {
                    info.setVersion(value);
                } else if ("name".equalsIgnoreCase(key)) {
                    info.setName(value);
                } else if ("description".equalsIgnoreCase(key)) {
                    info.setDescription(value);
                }
            }
        } finally {
            IOUtils.closeQuietly(br);
        }
    }

    private void scanPluginMethods() throws IOException, ClassNotFoundException {
        List<String> classes = JarUtils.getAllClassNames(mainJar);
        for (String clazz : classes) {
            Method[] methods = loader.loadClass(clazz).getMethods();
            if (methods == null || methods.length < 1) {
                continue;
            }
            for (Method method : methods) {
                Plugin anno = method.getAnnotation(Plugin.class);
                if (anno != null) {
                    if (pluginMethods.get(anno.extension()) == null) {
                        pluginMethods.put(anno.extension(), new ArrayList<>());
                    }
                    pluginMethods.get(anno.extension()).add(method);
                }
            }
        }
        LOGGER.debug(pluginMethods.size() + " plugin methods found");
    }

    public List<Method> getPluginMethod(String extension) {
        return pluginMethods.get(extension);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public PluginLoader getLoader() {
        return loader;
    }

    public void setLoader(PluginLoader loader) {
        this.loader = loader;
    }

    public String getMainJar() {
        return mainJar;
    }

    public void setMainJar(String mainJar) {
        this.mainJar = mainJar;
    }

    public String getArtifactId() {
        return artifactId;
    }

    public void setArtifactId(String artifactId) {
        this.artifactId = artifactId;
    }

    // this is usually used while serializing
    public Map<String, String> getPluginMethods() {
        Map<String, String> map = new HashMap<>();
        Set<Map.Entry<String, List<Method>>> methodSet = pluginMethods.entrySet();
        for (Map.Entry<String, List<Method>> entry : methodSet) {
            String key = entry.getKey();
            List<Method> methods = entry.getValue();
            StringBuffer buf = new StringBuffer();
            for (int j = 0; j < methods.size(); j++) {
                buf.append(MethodUtils.getSignature(methods.get(j)));
                if (j < methods.size() - 1) {
                    buf.append(" | ");
                }
            }
            map.put(key, new String(buf));
        }
        return map;
    }

    public List<String> getInstallSqls() {
        return installSqls;
    }

    public void setInstallSqls(List<String> installSqls) {
        this.installSqls = installSqls;
    }

    public List<String> getUninstallSqls() {
        return uninstallSqls;
    }

    public void setUninstallSqls(List<String> uninstallSqls) {
        this.uninstallSqls = uninstallSqls;
    }
}
